package org.gap.jseed.bean;

import static org.gap.jseed.utils.StringUtils.startsWith;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import javassist.CtClass;

import org.gap.jseed.Injector;
import org.gap.jseed.bean.annotation.Bean;
import org.gap.jseed.injection.ClassWriter;
import org.gap.jseed.reflect.ClassReflector;

/**
 * Use direct code manipulation (thus the code is bulkier than what it should
 * be, but should perform as if it were written by hand.
 * 
 * @author gpelcha
 */
public class BeanBehaviorInjector implements Injector {
	private static final String EVENT_LISTENER_LIST = "propertyChangeSupport";

	@Override
	public void inject(ClassWriter writer, Class<?> source) {
		ClassReflector clazz = new ClassReflector(source);
		if (clazz.hasClassAnnotation(Bean.class)) {
			injectBeanCode(clazz, writer);
		}
	}

	private void injectBeanCode(ClassReflector clazz, ClassWriter writer) {
		injectListenerMethods(clazz, writer);
		injectSetGetMethods(clazz, writer);
	}

	private <T> void injectListenerMethods(ClassReflector clazz,
			ClassWriter writer) {
		assureEventListenerExists(writer);
		for (Method each : clazz.methodIterator()) {
			if (isAbstractMethod(each)) {
				attemptToWriteListenerMethod(writer, each);
			}
		}
	}

	private void assureEventListenerExists(ClassWriter writer) {
		if (!writer.isFieldExisting(EVENT_LISTENER_LIST)) {
			writer.addFieldWithDefaultValue(PropertyChangeSupport.class,
					EVENT_LISTENER_LIST,
					"new java.beans.PropertyChangeSupport($0)");
		}
	}

	private boolean isAbstractMethod(Method each) {
		return Modifier.isAbstract(each.getModifiers());
	}

	private void attemptToWriteListenerMethod(ClassWriter writer,
			Method theMethod) {
		if (isAddListenerMethod(theMethod)) {
			writer.writeBody(theMethod, EVENT_LISTENER_LIST
					+ ".addPropertyChangeListener($1);");
		} else if (isRemoveListenerMethod(theMethod)) {
			writer.writeBody(theMethod, EVENT_LISTENER_LIST
					+ ".removePropertyChangeListener($1);");
		}
	}

	private boolean isAddListenerMethod(Method theMethod) {
		String methodName = theMethod.getName();
		return methodName.startsWith("add")
				&& isPropertyChangeListener(theMethod);
	}

	private boolean isRemoveListenerMethod(Method theMethod) {
		String methodName = theMethod.getName();
		return methodName.startsWith("remove")
				&& isPropertyChangeListener(theMethod);
	}

	private boolean isPropertyChangeListener(Method theMethod) {
		if (theMethod.getParameterTypes().length != 1) {
			return false;
		}
		Class<?> parameter = theMethod.getParameterTypes()[0];
		return PropertyChangeListener.class.isAssignableFrom(parameter);
	}

	private <T> void injectSetGetMethods(ClassReflector clazz,
			ClassWriter writer) {
		for (Method each : clazz.methodIterator()) {
			if (isAbstractMethod(each)) {
				attemptToWriteSetGetMethod(writer, each);
			}
		}
	}

	private void attemptToWriteSetGetMethod(ClassWriter writer, Method theMethod) {
		String property = getPropertyNameFrom(theMethod);
		if (isGetMethod(theMethod)) {
			assureFieldExists(writer, theMethod.getReturnType(), property);
			writer.writeBody(theMethod, "{ return " + property + "; }");
		} else if (isSetMethod(theMethod)) {
			assureFieldExists(writer, theMethod.getParameterTypes()[0],
					property);
			writer.writeBody(theMethod,
					addPropertyChangeListener(writer, property) + "    "
							+ property + " = $1;");
		}
	}

	private void assureFieldExists(ClassWriter writer, Class<?> propertyType,
			String property) {
		if (!writer.isFieldExisting(property)) {
			writer.addField(propertyType, property);
		}
	}

	private String addPropertyChangeListener(ClassWriter writer, String property) {
		String result = "";
			String fireEventLine = "if (" + EVENT_LISTENER_LIST + " != null) {\n    " + EVENT_LISTENER_LIST + ".firePropertyChange(\"%s\", %s, %s);\n}\n";
			if (writer.isFieldPrimitiveType(property)) {
				CtClass fieldType = writer.getFieldType(property);
				String oldValue = convertPrimitiveToObject(writer, fieldType, property);
				String newValue = convertPrimitiveToObject(writer, fieldType, "$1");
				result = String.format(fireEventLine, property, oldValue, newValue);
			} else {
				result = String.format(fireEventLine, property, property, "$1");
			}
		return result;
	}

	private String convertPrimitiveToObject(ClassWriter writer,
			CtClass fieldType, String property) {
		String objectName = getPrimitivesComplimentAsAnObject(fieldType);
		return String.format("%s.valueOf(%s)", objectName, property);
	}

	private static String getPrimitivesComplimentAsAnObject(
			CtClass primitiveType) {
		String primitiveName = primitiveType.getName();
		if (primitiveName.equals("int"))
			return "Integer";
		else if (primitiveName.equals("boolean"))
			return "Boolean";
		else if (primitiveName.equals("long"))
			return "Long";
		else if (primitiveName.equals("char"))
			return "Byte";
		else if (primitiveName.equals("double"))
			return "Double";
		else if (primitiveName.equals("float"))
			return "Float";
		else if (primitiveName.equals("byte"))
			return "Byte";
		return "";
	}

	/**
	 * Extracts the property from the method
	 * 
	 * @param each
	 * @return
	 */
	public static String getPropertyNameFrom(Method method) {
		String name = method.getName();
		if (startsWith(name, "is")) {
			name = name.substring(2);
		} else if (startsWith(name, "set", "get", "add")) {
			name = name.substring(3);
		} else if (startsWith(name, "remove")) {
			name = name.substring("remove".length());
		}
		return name;
	}

	private boolean isGetMethod(Method method) {
		return startsWith(method.getName(), "get", "is");
	}

	private boolean isSetMethod(Method method) {
		return startsWith(method.getName(), "set");
	}
}
